package com.zenika.zenbinding.client.model;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.user.client.ui.Widget;
import com.zenika.zenbinding.client.converter.Converter;
import com.zenika.zenbinding.client.exception.ConverterException;
import com.zenika.zenbinding.client.exception.DataException;
import com.zenika.zenbinding.client.exception.DataMessage;
import com.zenika.zenbinding.client.exception.ValidatorException;
import com.zenika.zenbinding.client.feedback.FeedbackManager;
import com.zenika.zenbinding.client.handler.Handler;
import com.zenika.zenbinding.client.handler.ListBindingListener;
import com.zenika.zenbinding.client.util.BeanUtil;
import com.zenika.zenbinding.client.validator.ValidationContext;
import com.zenika.zenbinding.client.validator.Validator;

/**
 * A ListModel manages the access to a property of a BusinessObject, the validation and redirect errors of Validation or Conversion to a FeedbackManager. It is used in the case of the property has to
 * be synchronized with a selection of objects in a list. The property could be an Array, a List or an Object.
 * 
 * @author Zenika
 */
public class ListModel implements Model, ListBindingListener {
  
  private BusinessObject source;
  private String propertyPath;
  private List<Validator> validators;
  private FeedbackManager feedback;
  private boolean errorState;
  private DataMessage currentError;
  private boolean single;
  private String feedbackId;
  private Converter converter;
  private Handler handler;
  
  /**
   * Build a ListModel.
   * 
   * @param source
   *          Object containing the property to bind
   * @param propertyPath
   *          Path to access to the property
   */
  public ListModel(BusinessObject source, String propertyPath) {
    this.source = source;
    this.propertyPath = propertyPath;
    this.validators = new ArrayList<Validator>();
  }
  
  /**
   * Build a ListModel.
   * 
   * @param source
   *          Object containing the property to bind
   * @param propertyPath
   *          Path to access to the property
   */
  public ListModel(BusinessObject source, String propertyPath, Converter converter) {
    this.source = source;
    this.propertyPath = propertyPath;
    this.converter = converter;
    this.validators = new ArrayList<Validator>();
  }
  
  /**
   * Return the list of objects contained in the property.
   * 
   * @return List
   */
  public List<?> getList(Handler handler) {
    Object object = null;
    
    try {
      object = BeanUtil.getProperty(this.source, this.propertyPath);
    }
    catch ( DataException e1 ) {
      e1.printStackTrace();
    }
    
    if ( object instanceof List ) {
      this.single = false;
      List<?> list = (List<?>) object;
      return list;
    } else {
      this.single = true;
      List<Object> list = new ArrayList<Object>();
      
      try {
        if ( this.converter != null )
          list.add(this.converter.fromObjectToString(object));
        else
          list.add(object);
        return list;
        
      }
      catch ( ConverterException e ) {
        e.printStackTrace();
        // FIXME
        // return list;
        return null;
      }
    }
  }
  
  /**
   * Update the property from the list selected by the user.
   * 
   * @param selectedItems
   *          SList of selected items
   * @param handler
   *          The handler which had sent the selection
   */
  public void updateList(List<?> selectedItems, Handler handler) {
    try {
      // try to validate the new value
      this.validate(selectedItems);
      
      if ( this.errorState ) {
        this.errorState = false;
        this.currentError = null;
        this.fireErrorStop(handler);
      }
      
      // update the model, everything went fine
      this.updateValue(selectedItems);
      
    }
    catch ( ValidatorException e ) {
      // still update the model
      this.updateValue(selectedItems);
      
      if ( !this.errorState ) {
        this.errorState = true;
        this.currentError = e.getDataMessage();
        this.fireError(this.currentError, handler);
      }
    }
  }
  
  private void updateValue(List<?> selectedItems) {
    try {
      if ( this.single ) {
        if ( this.converter != null ) {
          BeanUtil.setProperty(this.source, this.propertyPath, this.converter.fromStringToObject((String) selectedItems.get(0)));
        } else {
          BeanUtil.setProperty(this.source, this.propertyPath, selectedItems.get(0));
        }
        
      } else {
        BeanUtil.setProperty(this.source, this.propertyPath, selectedItems);
      }
      
    }
    catch ( ConverterException e ) {
      if ( !this.errorState ) {
        this.errorState = true;
        this.currentError = e.getDataMessage();
        this.fireError(this.currentError, handler);
      }
    }
  }
  
  /**
   * Validate the object. If the validation fails, a ValidatorException is thrown.
   * 
   * @param object
   *          Object to validate
   * @throws ValidatorException
   */
  public void validate(List<?> selectedItems) throws ValidatorException {
    Iterator<Validator> iter = this.validators.iterator();
    while ( iter.hasNext() ) {
      iter.next().validate(selectedItems);
    }
  }
  
  public void onChange(List<?> selectedItems, Handler handler) {
    this.updateList(selectedItems, handler);
  }
  
  public void setFeedbackManager(FeedbackManager manager) {
    this.feedback = manager;
  }
  
  public void addValidator(Validator validator) {
    this.validators.add(validator);
  }
  
  public void addValidator(ValidationContext context, Validator validator) {
    if ( validator != null )
      this.validators.add(validator);
    context.addModel(this);
  }
  
  public void fireError(DataMessage message, Handler handler) {
    if ( this.feedbackId != null ) {
      message.setMessage(message.getMessage() + " : " + this.feedbackId);
    }
    this.feedback.enterErrorState(message, handler);
  }
  
  public void fireErrorStop(Handler handler) {
    this.feedback.leaveErrorState(handler);
  }
  
  public void setFeedbackManager(FeedbackManager manager, String feedbackId) {
    this.setFeedbackManager(manager);
    this.feedbackId = feedbackId;
  }
  
  /**
   * Specify the converter to use with this model. If you don't do it, a converter is automatically selected.
   * 
   * @param converter
   *          Converter
   */
  public void setConverter(Converter converter) {
    this.converter = converter;
  }
  
  public void setHandler(Handler handler) {
    this.handler = handler;
  }
  
  public Handler getHandler() {
    return this.handler;
  }
  
  public Widget getWidget() {
    return this.handler == null ? null : this.handler.getWidget();
  }
  
  public DataMessage getCurrentValidationState() {
    return this.currentError;
  }
  
  public DataMessage validate() {
    try {
      this.validate(this.getList(this.handler));
      return null;
      
    }
    catch ( ValidatorException ve ) {
      if ( this.feedbackId != null && ve.getDataMessage() != null ) {
        ve.getDataMessage().setMessage(ve.getDataMessage().getMessage() + " : " + this.feedbackId);
      }
      return ve.getDataMessage();
    }
  }
  
}
